package com.rslai.commons.util.redmine;

import com.rslai.commons.util.StringUtil;
import com.taskadapter.redmineapi.*;
import com.taskadapter.redmineapi.bean.*;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * redmine 工具类
 *
 * 包装 redmine 开源项目 redmine-java-api，实现 bug 搜索、创建、状态修改等功能
 * redmine-java-api 项目的 github 地址: https://github.com/taskadapter/redmine-java-api
 *
 * redmine api 参考 https://www.redmine.org/projects/redmine/wiki/Rest_Issues
 */
public class RedmineHelper {
    private static ConcurrentHashMap<String, RedmineHelper> redmineHelperMap = new ConcurrentHashMap<>(); // 保存 RedmineHelper 对象（key 是 链接唯一值）

    private String url = ""; // redmine 服务器地址
    private String adminApiAccessKey = ""; // redmine 后台配置的 api 密钥（系统管理员）。当创建项目等会使用此 key
    private String userApiAccessKey = ""; // redmine 后台配置的 api 密钥（普通用户（当前用户））。当创建、更改 issue 等会使用此 key
    private int pageSize = 10; // 分页列表时，每页显示记录数

    private RedmineManager adminRedmineManager; // RedmineManager（系统管理员）
    private RedmineManager userRedmineManager; // RedmineManager（普通用户（当前用户））

    /**
     * 禁用构造函数
     */
    private RedmineHelper() {

    }

    /**
     * 构造函数
     * @param url redmine 服务器地址
     * @param adminApiAccessKey redmine 后台配置的 api 密钥（系统管理员）。当创建项目等会使用此 key
     * @param userApiAccessKey redmine 后台配置的 api 密钥（普通用户（当前用户））。当创建、更改 issue 等会使用此 key
     * @param pageSize 分页列表时，每页显示记录数
     */
    private RedmineHelper(String url, String adminApiAccessKey, String userApiAccessKey, int pageSize) {
        if (StringUtil.isBlank(url)) throw new RuntimeException("url 不能为空");
        if (StringUtil.isBlank(adminApiAccessKey) && StringUtil.isBlank(userApiAccessKey)) throw new RuntimeException("adminApiAccessKey、userApiAccessKey 不能同时为空");

        this.url = url;
        if (StringUtil.isNotBlank(adminApiAccessKey)) {
            this.adminApiAccessKey = adminApiAccessKey;
        } else {
            this.adminApiAccessKey = userApiAccessKey;
        }
        if (StringUtil.isNotBlank(userApiAccessKey)) {
            this.userApiAccessKey = userApiAccessKey;
        } else {
            this.userApiAccessKey = adminApiAccessKey;
        }
        if (pageSize > 0) {
            this.pageSize = pageSize;
        }
    }

    /**
     * 返回一个 RedmineHelper 实例
     * @param url redmine 服务器地址
     * @param adminApiAccessKey redmine 后台配置的 api 密钥（系统管理员）。当创建项目等会使用此 key
     * @param userApiAccessKey redmine 后台配置的 api 密钥（普通用户（当前用户））。当创建、更改 issue 等会使用此 key
     * @param pageSize 分页列表时，每页显示记录数
     * @return
     */
    static public RedmineHelper getRedmineHelper(String url, String adminApiAccessKey, String userApiAccessKey, int pageSize) {
        String key = url + "|" + adminApiAccessKey + "|" + userApiAccessKey + "|" + pageSize;
        RedmineHelper redmineHelper = redmineHelperMap.get(key);

        if (redmineHelper == null) {
            redmineHelper = new RedmineHelper(url, adminApiAccessKey, userApiAccessKey, pageSize);
            redmineHelperMap.put(key, redmineHelper);
        }
        return redmineHelper;
    }

    /**
     * 使用用户角色，搜索 issue
     * @param projectName 项目名称，例如 https://redmine.xxx.com/projects/uip 其中 uip 就是 项目ID
     * @param parentId 父任务ID，如果没有父任务传 null
     * @param issueId bug id，如果想一次查询多个 bug id，中间以逗号分割。例如："9455" 或 "9455,9456"
     * @param status 仅获取指定状态的 issue(open：打开状态的 issue、closed: 关闭状态的 issue、*: 打开+关闭状态的 issue)。可以为 null，为 null 查询 open 状态
     * @param pageNum 页号，从 1 开始
     * @return
     * @throws RedmineException
     */
    public List<Issue> searchIssueByUser(String projectName, Integer parentId, String issueId, String status, int pageNum) throws RedmineException {
        RedmineManager mgr = getUserRedmineManager();
        return searchIssue(mgr, projectName, parentId, issueId, status, pageNum);
    }

    /**
     * 使用管理员角色，搜索 issue
     * @param projectName 项目名称，例如 https://redmine.xxx.com/projects/uip 其中 uip 就是 项目ID
     * @param parentId 父任务ID，如果没有父任务传 null
     * @param issueId bug id，如果想一次查询多个 bug id，中间以逗号分割。例如："9455" 或 "9455,9456"
     * @param status 仅获取指定状态的 issue(open：打开状态的 issue、closed: 关闭状态的 issue、*: 打开+关闭状态的 issue)。可以为 null，为 null 查询 open 状态
     * @param pageNum 页号，从 1 开始
     * @return
     * @throws RedmineException
     */
    public List<Issue> searchIssueByAdmin(String projectName, Integer parentId, String issueId, String status, int pageNum) throws RedmineException {
        RedmineManager mgr = getAdminRedmineManager();
        return searchIssue(mgr, projectName, parentId, issueId, status, pageNum);
    }

    /**
     * 搜索 issue
     * @param redmineManager RedmineManager 对象
     * @param projectName 项目名称，例如 https://redmine.xxx.com/projects/uip 其中 uip 就是 项目ID
     * @param parentId 父任务ID，如果没有父任务传 null
     * @param issueId bug id，如果想一次查询多个 bug id，中间以逗号分割。例如："9455" 或 "9455,9456"
     * @param status 仅获取指定状态的 issue(open：打开状态的 issue、closed: 关闭状态的 issue、*: 打开+关闭状态的 issue)。可以为 null，为 null 查询 open 状态
     * @param pageNum 页号，从 1 开始
     * @return
     * @throws RedmineException
     */
    private List<Issue> searchIssue(RedmineManager redmineManager, String projectName, Integer parentId, String issueId, String status, int pageNum) throws RedmineException {
        Params params = new Params();
        params.add("offset", this.getOffset(pageNum));
        params.add("limit", this.getLimit());

        if (StringUtil.isNotBlank(projectName)) params.add("project_id", projectName);
        if (StringUtil.isNotBlank(issueId)) params.add("issue_id", issueId);
        if (parentId != null) params.add("parent_id", parentId.toString());

        // issue 状态
        if (StringUtil.isNotBlank(status)) {
            switch (status) {
                case "open":
                    params.add("status_id", "open");
                    break;
                case "closed":
                    params.add("status_id", "closed");
                    break;
                case "*":
                    params.add("status_id", "*");
                    break;
            }
        }

        return redmineManager.getIssueManager().getIssues(params).getResults();
    }

    /**
     * 创建 issue
     * @param projectName 项目名称。不可以为 null
     * @param trackerId 跟踪（Bug : 1; Feature : 2; Support : 3）。可以为 null，默认 Feature
     * @param parentId 父任务ID。可以为 null，默认没有父任务
     * @param categoryId 类别ID，可以用类别搜索或根据类别指派给某人（在项目 配置 -> 问题类别 中新增、修改和删除）。可以为 null，默认没有类别
     * @param statusId 状态（New : 1; In Progress : 2; Resolved : 3; Feedback : 4; Closed : 5; Rejected : 6; Testing : 9; Reopened : 11; Deferred : 12）。可以为空，默认 New
     * @param priorityId 优先级（Low : 1; Normal : 2; High : 3; Urgent : 4; Immediate : 5; Pending : 12）。可以为 null，默认为 Normal
     * @param assigneeId 指派给。可以为空，默认没有指派人（注意，当此字段为 null 且设置了 categoryId，会用类别中的指派人）
     * @param versionId 版本ID。可以为 null，默认没有版本
     * @param watchers 关注者，多个关注者逗号分割，例如：31,162。可以为 null，默认没有关注者
     * @param subject 主题。不可以为 null
     * @param description 描述，可以为 null
     * @return
     * @throws RedmineException
     */
    public Issue createIssue(String projectName, Integer trackerId, Integer parentId, Integer categoryId, Integer statusId, Integer priorityId, Integer assigneeId, Integer versionId, String watchers, String subject, String description) throws RedmineException {
        if (StringUtil.isBlank(projectName)) throw new RedmineProcessingException(new ArrayList<>(Arrays.asList("项目 不能为空字符")));

        RedmineManager mgr = getUserRedmineManager();
        Project project = this.getProjectByName(projectName); // 查找项目
        if (project == null) throw new NotFoundException("Create issue failed, Project name '" + projectName + "' is not found.");

        Issue issue = new Issue(mgr.getTransport(), project.getId()) // new Issue
                .setProjectId(project.getId())
                .setSubject(subject);

        if (StringUtil.isNotBlank(description)) issue.setDescription(description); // 描述
        if (assigneeId != null && assigneeId != 0) issue.setAssigneeId(assigneeId); // 指派给
        if (priorityId != null && priorityId != 0) issue.setPriorityId(3); // 优先级
        if (parentId != null && parentId != 0) issue.setParentId(parentId); // 父任务
        if (statusId != null && statusId != 0) issue.setStatusId(statusId); // 状态

        if (trackerId != null && trackerId != 0) { // 跟踪
            Tracker tracker = new Tracker();
            tracker.setId(trackerId);
            issue.setTracker(tracker);
        }

        if (categoryId != null && categoryId != 0) { // 类别
            IssueCategory category = new IssueCategory(mgr.getTransport()).setId(categoryId);
            issue.setCategory(category);
        }

        if (versionId != null && versionId != 0) { // 目标版本
            Version version = new Version().setId(versionId);
            issue.setTargetVersion(version);
        }

        if (StringUtil.isNotBlank(watchers)) { // 关注者
            Collection<Watcher> watcherList = new ArrayList<>();
            for (String s : watchers.split(",")) {
                if (StringUtil.isNotBlank(s)) {
                    try {
                        Watcher w = new Watcher();
                        w.setId(Integer.valueOf(s));
                        watcherList.add(w);
                    } catch (NumberFormatException e) {

                    }
                }
            }
            if (watcherList.size() != 0) {
                issue.addWatchers(watcherList);
            }
        }

        Issue ret = issue.create();
        return ret;
    }

    /**
     * 更新 issue
     * @param issueId issue ID
     * @param statusId 状态（New : 1; In Progress : 2; Resolved : 3; Feedback : 4; Closed : 5; Rejected : 6; Testing : 9; Reopened : 11; Deferred : 12）。可以为空，为空不修改
     * @param assigneeId 指派给。可以为空，为空不修改
     * @param versionId 版本ID。可以为 null，为空不修改
     * @param estimatedHours 预期时间。可以为 null，为空不修改
     * @param watchers 关注者，多个关注者逗号分割，例如：31,162。可以为 null，为空不修改
     * @param notes 说明，可以为 null，为空不添加说明
     * @param subject 主题。可以为 null，为空不修改
     * @param description 描述，可以为 null，为空不修改
     * @return
     * @throws RedmineException
     */
    public boolean updateIssueById(int issueId, Integer statusId, Integer assigneeId, Integer versionId, Float estimatedHours, String watchers, Map<String, String> customField, String notes, String subject, String description) throws RedmineException {
        RedmineManager mgr = getUserRedmineManager();
        Issue issue = this.getIssueById(mgr, issueId); // 查询 issue
        if (issue == null) throw new NotFoundException("Update issue failed, Issue id '" + issueId + "' is not found.");

        if (statusId != null && statusId != 0) issue.setStatusId(statusId); // 状态
        if (assigneeId != null && assigneeId != 0) issue.setAssigneeId(assigneeId); // 指派给
        if (estimatedHours != null) issue.setEstimatedHours(estimatedHours); // 预期时间
        if (StringUtil.isNotBlank(notes)) issue.setNotes(notes); // 说明
        if (StringUtil.isNotBlank(subject)) issue.setSubject(subject); // 主题
        if (StringUtil.isNotBlank(description)) issue.setDescription(description); // 描述

        if (versionId != null && versionId != 0) { // 目标版本
            Version version = new Version().setId(versionId);
            issue.setTargetVersion(version);
        }

        if (StringUtil.isNotBlank(watchers)) { // 关注者
            for (String s : watchers.split(",")) {
                if (StringUtil.isNotBlank(s)) {
                    try {
                        issue.addWatcher(Integer.valueOf(s));
                    } catch (NumberFormatException e) {

                    }
                }
            }
        }

        if (customField != null) { // 自定义字段
            for (Map.Entry<String, String> entry : customField.entrySet()) {
                CustomField field = issue.getCustomFieldByName(entry.getKey());
                if (field == null) {
                    throw new RuntimeException("Custom Field '" + entry.getKey() + "' is not found.");
                }
                field.setValue(entry.getValue());
            }
        }

        issue.update(); // 更新 issue
        return true;
    }

    /**
     * 删除 issue
     * @param issueId issue ID
     * @return
     * @throws RedmineException
     */
    public boolean deleteIssueById(int issueId) throws RedmineException {
        RedmineManager mgr = getUserRedmineManager();
        Issue issue = this.getIssueById(mgr, issueId); // 查询 issue
        if (issue != null) issue.delete();
        return true;
    }

    /**
     * 使用用户角色，根据 issue id 返回一个 issue
     * @param issueId issue ID
     * @param include include 列表，如下:
     *      Include.journals : 日志（修订日志、说明）
     *      Include.relations : 相关的问题
     *      Include.attachments : 附件
     *      Include.changesets : 变更 - 但不知道操作什么会有 changesets
     *      Include.watchers : 关注者
     *      Include.children : 子任务
     * @return
     * @throws RedmineException
     */
    public Issue getIssueByUserRole(Integer issueId, Include... include) throws RedmineException {
        RedmineManager mgr = getUserRedmineManager();
        return this.getIssueById(mgr, issueId, include); // 查询 issue
    }

    /**
     * 使用管理员角色，根据 issue id 返回一个 issue
     * @param issueId issue ID
     * @param include include 列表，如下:
     *      Include.journals : 日志（修订日志、说明）
     *      Include.relations : 相关的问题
     *      Include.attachments : 附件
     *      Include.changesets : 变更 - 但不知道操作什么会有 changesets
     *      Include.watchers : 关注者
     *      Include.children : 子任务
     * @return
     * @throws RedmineException
     */
    public Issue getIssueByAdminRole(Integer issueId, Include... include) throws RedmineException {
        RedmineManager mgr = getAdminRedmineManager();
        return this.getIssueById(mgr, issueId, include); // 查询 issue
    }

    /**
     * 根据 issue id 返回一个 issue
     * @param redmineManager RedmineManager 对象
     * @param issueId issue ID
     * @param include include 列表，如下:
     *      Include.journals : 日志（修订日志、说明）
     *      Include.relations : 相关的问题
     *      Include.attachments : 附件
     *      Include.changesets : 变更 - 但不知道操作什么会有 changesets
     *      Include.watchers : 关注者
     *      Include.children : 子任务
     * @return
     * @throws RedmineException
     */
    private Issue getIssueById(RedmineManager redmineManager, Integer issueId, Include... include) throws RedmineException {
        IssueManager issueManager = redmineManager.getIssueManager();
        try {
            return issueManager.getIssueById(issueId, include);
        } catch (NotFoundException e) {
            return null;
        }
    }

    /**
     * 从服务器返回项目列表。此操作需要 Redmine管理员 权限
     * @return
     * @throws RedmineException
     */
    public List<Project> getProjects() throws RedmineException {
        RedmineManager mgr = getAdminRedmineManager();
        ProjectManager projectManager = mgr.getProjectManager();
        return projectManager.getProjects();
    }

    /**
     * 根据 项目名称 返回一个 project
     * @param projectName 项目名称
     * @return
     * @throws RedmineException
     */
    public Project getProjectByName(String projectName) throws RedmineException {
        try {
            RedmineManager mgr = getUserRedmineManager();
            ProjectManager projectManager = mgr.getProjectManager();
            return projectManager.getProjectByKey(projectName);
        } catch (NotFoundException e) {
            return null;
        }
    }

    /**
     * 根据 项目ID 从服务器返回此项目可以访问到的版本列表
     * @param projectID 项目ID
     * @return
     * @throws RedmineException
     */
    public List<Version> getVersions(int projectID) throws RedmineException {
        try {
            RedmineManager mgr = getUserRedmineManager();
            ProjectManager projectManager = mgr.getProjectManager();
            return projectManager.getVersions(projectID);
        } catch (NotFoundException e) {
            return null;
        }
    }

    /**
     * 根据 版本ID 返回一个 Version
     * @param versionId 版本ID
     * @return
     * @throws RedmineException
     */
    public Version getVersionById(int versionId) throws RedmineException {
        try {
            RedmineManager mgr = getUserRedmineManager();
            ProjectManager projectManager = mgr.getProjectManager();
            return projectManager.getVersionById(versionId);
        } catch (NotFoundException e) {
            return null;
        }
    }

    /**
     * 从服务器返回用户列表。此操作需要 Redmine管理员 权限
     * @return
     * @throws RedmineException
     */
    public List<User> getUsers() throws RedmineException {
        RedmineManager mgr = getAdminRedmineManager();
        UserManager userManager = mgr.getUserManager();
        return userManager.getUsers();
    }

    /**
     * 根据页号，返回偏移量（即从第几行开始返回结果）
     * @param pageNum 页号，从 1 开始
     * @return
     */
    private String getOffset(int pageNum) {
        if (pageNum > 0) {
            return String.valueOf(pageNum * pageSize - pageSize);
        } else {
            return "0";
        }
    }

    /**
     * 返回 limit（每页显示记录数）
     * @return
     */
    private String getLimit() {
        return String.valueOf(this.pageSize);
    }

    /**
     * 得到一个 RedmineManager（系统管理员）
     * @return
     */
    private RedmineManager getAdminRedmineManager() {
        if (adminRedmineManager == null) {
            adminRedmineManager = RedmineManagerFactory.createWithApiKey(this.url, this.adminApiAccessKey);
        }
        return adminRedmineManager;
    }

    /**
     * 得到一个 RedmineManager（普通用户（当前用户））
     * @return
     */
    private RedmineManager getUserRedmineManager() {
        if (userRedmineManager == null) {
            userRedmineManager = RedmineManagerFactory.createWithApiKey(this.url, this.userApiAccessKey);
        }
        return userRedmineManager;
    }

}
